/*  This program is free software; you can redistribute it and/or modify it
    under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation; either version 2.1 of the License, or (at
    your option) any later version.
    For more details, see the GNU Lesser General Public License (www.fsf.org
    or the COPYING file somewhere in the package)
 */

#include "Utils.hh"

/*! @file test/Utils.cc
    @brief Useful (non-inline) functions and class methods.
    @author @ref Guillaume_Terrissol
    @date 15th February 2010 - 10th March 2014
    @note This file is distributed under the LGPL license.
    Refer to the file COPYING (or http://www.fsf.org) for more information.
 */

#include <cstdlib>
#include <fstream>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <unistd.h>
#include <utime.h>
#include <sys/stat.h>

#include <Context.hh>
#include <Utils.hh>

namespace CHK
{
//------------------------------------------------------------------------------
//                                Redirect Stream
//------------------------------------------------------------------------------

    /*! @param pFileName Name fo the file to which redirect the stream <i>pStream</i> contents
        @param pStream   Stream to redirect
     */
    RedirectStream::RedirectStream(const char* pFileName, FILE* pStream)
        : mOrigin{fileno(pStream)}
        , mDuplicate{dup(mOrigin)}
        , mNewFile{freopen(pFileName, "wb+", pStream)}
    { }


    /*! Restores the original stream.
     */
    RedirectStream::~RedirectStream()
    {
        dup2(mDuplicate, mOrigin);
    }


//------------------------------------------------------------------------------
//                                 Command Dump
//------------------------------------------------------------------------------

    /*! @param pCommand System command to execute, in the current directory.  An instruction
        sequence is allowed, on a single line (see bash syntax)
        @note If @p pCommand failed, output() will return an empty string
     */
    CommandDump::CommandDump(const std::string& pCommand)
        : mOutput{}
    {
        RedirectStream  lRedirector{"dump.log", stdout};
        if (system(pCommand.c_str()) == 0)
        {
            std::fstream    lInfo{"dump.log", std::fstream::in | std::fstream::binary};
            while(!lInfo.eof())
            {
                std::string     lLine;
                getline(lInfo, lLine);

                mOutput += (mOutput.empty() ? "" : "\n") + lLine;
            }
        }
        remove("dump.log");
    }


    /*! @return The output text from the command passed as an argument in the constructor
     */
    const std::string& CommandDump::output() const
    {
        return mOutput;
    }


//------------------------------------------------------------------------------
//                       File Handling Functions : Bundles
//------------------------------------------------------------------------------

    /*! @return The bundle path, as defined in osgi.ini
     */
    std::string bundleDir()
    {
        std::fstream    lConfig{"./osgi.ini", std::fstream::in | std::fstream::binary};
        if (!lConfig.is_open())
        {
            throw std::runtime_error{"Config file couldn't be opened"};
        }

        std::string     lOutDir{};
        while(!lConfig.eof() && lOutDir.empty())
        {
            std::string lLine;
            getline(lConfig, lLine);
            if (lLine.find("bundleDir") != std::string::npos)
            {
                size_t  lPos = lLine.find("=");
                if (lPos != std::string::npos)
                {
                    lOutDir = OSGi::trimString(lLine.substr(lPos + 1));
                }
            }
        }

        return lOutDir;
    }


    /*! Copies a bundle file in the bundle folder.
        @param pName Bundle to copy filename
     */
    void installBundle(const std::string& pName)
    {
        std::string     lInputFileName = std::string("../bundles/") + pName;
        std::fstream    lIn{lInputFileName.c_str(), std::fstream::in  | std::fstream::binary};
        if (!lIn.is_open())
        {
            throw std::runtime_error("Couldn't open input file " + lInputFileName);
        }

        std::string     lOutputFileName = bundleDir() + "/" + pName;
        std::fstream    lOut{lOutputFileName.c_str(), std::fstream::out | std::fstream::binary};
        if (!lOut.is_open())
        {
            throw std::runtime_error("Couldn't open output file " + lOutputFileName);
        }

        lOut << lIn.rdbuf();
        lOut.close();

        struct stat lStats;
        stat(lInputFileName.c_str(), &lStats);
        struct utimbuf lTimeBuf{ lStats.st_mtime, lStats.st_atime };
        utime(lOutputFileName.c_str(), &lTimeBuf);
    }


    /*! Removes all bundles from the bundle folder.
     */
    void removeBundles()
    {
        std::string lOutDir = bundleDir();
        if (lOutDir.empty())
        {
            throw std::runtime_error("Invalid bundle directory");
        }
        OSGi::emptyDirectory(lOutDir);
    }


//------------------------------------------------------------------------------
//                       File Handling Functions : Common
//------------------------------------------------------------------------------

    /*! @param pL Name of the 1st file
        @param pR Name of the 2nd file
        @retval true if both files are identical
        @retval false otherwise
     */
    bool compareFiles(const std::string& pL, const std::string& pR)
    {
        std::fstream    lL{pL.c_str(), std::fstream::in | std::fstream::binary};
        std::fstream    lR{pR.c_str(), std::fstream::in | std::fstream::binary};

        if (!lL.is_open())
        {
            throw std::runtime_error{"File " + pL + " unreadable"};
        }
        if (!lR.is_open())
        {
            throw std::runtime_error{"File " + pR + " unreadable"};
        }

        while(!lL.eof() && !lR.eof())
        {
            std::string lLLine;
            std::string lRLine;
            std::getline(lL, lLLine);
            std::getline(lR, lRLine);
            if (lLLine != lRLine)
            {
                return false;
            }
        }

        return lL.eof() && lR.eof();
    }


    /*! @param pName File the emptiness of which to check
        @retval true if the file is empty
        @retval false otherwise
     */
    bool isFileEmpty(const std::string& pName)
    {
        std::fstream    lFile{pName.c_str(), std::fstream::in | std::fstream::binary};
        if (!lFile.is_open())
        {
            throw std::runtime_error{"File " + pName + " unreadable"};
        }
        lFile.seekg(0, std::fstream::end);

        return (lFile.tellg() == 0);
    }


//------------------------------------------------------------------------------
//                             Life Cycle Constants
//------------------------------------------------------------------------------

    const std::string   kSuspended  = "suspended";
    const std::string   kPause      = "pause";
    const std::string   kResume     = "resume";

//------------------------------------------------------------------------------
//                             Valid Bundle Factory
//------------------------------------------------------------------------------

    /*! Defaulted.
     */
    ValidBundleFactory::ValidBundleFactory() = default;


    /*! Defaulted.
     */
    ValidBundleFactory::~ValidBundleFactory() = default;


    /*! Tunes the life cycle for ValidBundle instances.
        @param pLifeCycle LifeCycle instance to work with
     */
    void ValidBundleFactory::setUp(OSGi::LifeCycle::Ptr pLifeCycle) const
    {
        BundleFactory::setUp(pLifeCycle);

        pLifeCycle->declareState(kSuspended);

        pLifeCycle->declareChange(kPause,  OSGi::kActive, kSuspended,    &ValidBundle::suspend, OSGi::LifeCycle::ERecurse::eThisLast);
        pLifeCycle->declareChange(kResume, kSuspended,    OSGi::kActive, &ValidBundle::resume,  OSGi::LifeCycle::ERecurse::eThisFirst);
    }


    /*! Creates a ValidBundle instance.
        @copydoc OSGi::BundleFactory::makeBundle(std::string, int, ContextCPtr) const
     */
    OSGi::Bundle::Ptr ValidBundleFactory::doMakeBundle(std::string pFilename, int pId, ContextCPtr pContext) const
    {
        return std::make_shared<ValidBundle>(pFilename, pId, pContext);
    }


//------------------------------------------------------------------------------
//                                 Valid Bundle
//------------------------------------------------------------------------------

    /*! @param pFileName Bundle filename
        @param pId       Bundle Id
        @param pContext  Execution context
     */
    ValidBundle::ValidBundle(std::string pFileName, int pId, const ContextCPtr pContext)
        : Bundle{pFileName, pId, pContext, Bundle::Key{}}
        , mContext{pContext}
        , mIsPaused{false}
    {
        if (state() == OSGi::kInstalled)
        {
            mContext->out() << "Installed " << name() << " bundle" << std::endl;
        }
        else
        {
            mContext->err() << "Failed to install bundle " << pFileName << std::endl;
        }
    }


    /*! Just a small trace.
     */
    ValidBundle::~ValidBundle()
    {
        mContext->out() << "Destroyed " << name() << " bundle" << std::endl;
    }


    /*! @param pContext Unused
        @note The ContextPTr parameter is just here for consistency with other "action" methods (see start(), stop(), etc.)
     */
    bool ValidBundle::suspend(ContextPtr)
    {
        if (state() == OSGi::kActive)
        {
            mContext->out() << name() << " is now paused" << std::endl;
            return true;
        }
        else
        {
            return false;
        }
    }


    /*! @param pContext Unused
        @note The ContextPTr parameter is just here for consistency with other "action" methods (see start(), stop(), etc.)
     */
    bool ValidBundle::resume(ContextPtr)
    {
        if (state() == kSuspended)
        {
            mContext->out() << "Resuming " << name() << " ..." << std::endl;
            return true;
        }
        else
        {
            return false;
        }
    }
}
